基础环境介绍
NIM_P2P_SDK 基于 Qt 5.8(MSVC2013)编写,提供了一整套 C 风格接口的文件传输功能。
NIM_P2P_DEVELOP_KIT 是对导出的 C 接口的一层 C++ 封装。可导入到自己项目中编译为静态库,并设置为应用项目引用项。
目前NIM_P2P_SDK只提供了局域网内直连文件传输方案,开发者也可以接入自定义的传输方案,需要向SDK进行注册 由于使用QT5.8版本,该模块不支持windows xp系统
业务介绍
NIM P2P SDK 封装了网络文件传输的基础功能,在传输文件前需要协商传输什么文件、用什么方式传输、接收还是拒绝等信息。这些信息 SDK 是不具备单独的消息通道能力的,但 SDK 提供了注册消息协商通道的回调接口用于传送文件时的各类业务协商工作,上层应用可以使用任何一种消息通道来实现文件传输。
下图描述了 P2P 文件传输过程中的整体流程,从发起到接收直至最后的开始传输文件,可以根据数字标号查看调用顺序。一个完整的流程是应用 A 调用 SDK 接口主动发起文件传输请求,此时 SDK 内部封装好消息信息回调到上层注册好的发送消息回调函数,该函数中可以利用任何消息通道给应用 B 发送消息(这里我们使用 IM 消息通道),发送消息前要标记清楚是传输文件的请求。
当应用 B 收到消息时判断是传送我文件的请求,那么将该消息解包并传递给 SDK,SDK 会根据消息内容回调到应用 B 注册的接收到文件传输请求的回调,此时可以根据需要在应用界面做对应展示。如接收、另存、拒绝等按钮的展示。
当应用 B 点击接收文件时调用 SDK 提供的接收文件方法,此时 SDK 会包装消息并回调到应用 B 注册的发送消息回调函数,再将消息包装成自己发送消息通道的结构传送给应用 A。
最后应用 A 收到接收或者拒绝的消息传递给 SDK,最终由 SDK 做决策,当完全协商结束后 SDK 底层进行文件传输,与上层完全脱离关系。
接入步骤
初始化
初始化工作一般建议在应用运行时就初始化好,在 SDK 初始化过程中,我们需要指定清楚 4 个回调函数,分别是
- 接收到文件传输请求的回调函数
- 发送消息时所需的回调函数
- 传输过程中文件状态变化的回调函数
- 传输过程中传输进度变化的回调函数
除了几个回调,我们还要指定当前 NIM_P2P_SDK 的动态库文件路径。
以下为初始化示例代码,初始化完成后 SDK 会返回一个接收消息的通道函数指针。在 C++ 封装层中我们保存了这个指针,当需要给 SDK 传递信息时调用该回调函数传递对应数据就可以了。
//注册P2P组件回调
QPath::AddNewEnvironment(QPath::GetAppPath() + L"p2p");
if (!nim_p2p::NimP2PDvelopKit::GetInstance()->Init(QPath::GetAppPath() + L"p2p",
nim_comp::P2PCallback::OnTransferFileRequest,
nim_comp::P2PCallback::OnTransferFileSessionStateChangeCallback,
nim_comp::P2PCallback::OnTransferFileProgressCallback,
nim_comp::P2PCallback::SendCommandChannel))
{
QLOG_ERR(L"Failed to init nim p2p develop kit.");
}
发起文件传输请求
使用 SDK 提供的 TransferFile 接口来发起传送文件请求
transfer_file_session_id = nim_p2p::NimP2PDvelopKit::GetInstance()->TransferFile((RemoteFlagType)session_id_.c_str(), file);
if (!transfer_file_session_id)
{
QLOG_ERR(L"Failed to transfer file to {0}") << session_id_.c_str();
AddTextTip(L"传送文件失败,不支持 P2P 文件传送或加载模块失败!");
return;
}
调用该函数后,SDK 会返回一个文件传输的 session id,我们可以记录这个 ID 来做上层的业务处理。比如在界面中添加一个传输文件的 bubble,bubble 所关联的文件传输 session id 就是这里返回的 session id。当后面收到其他关于此 session id 相关的文件传输状态变化,即可根据 session id 找到这个 bubble 来改变状态。
当调用传送文件接口后,SDK 会回调到上层最初我们注册好的 发送消息时所需的回调函数
,对应上面示例代码中的 nim_comp::P2PCallback::SendCommandChannel
,在这个回调中,我们包装 SDK 返回的消息(包含传输文件的请求,文件名、大小、session id 等信息)通过 IM 消息通道将消息发送给另一端。
bool P2PCallback::SendCommandChannel(const RemoteFlagType remote_flag, const char* const command)
{
QLOG_APP(L"[P2PCallback::SendCommandChannel] Send new command to {0}, command {1}") << remote_flag << command;
Json::Value values;
Json::Reader reader;
if (reader.parse(command, values))
{
nim::IMMessage msg;
std::string session_id = values["session_id"].asString();
ASSERT(!session_id.empty());
// 增加 sub type 类型为使用 P2P 传输文件
values["type"] = CustomMsgType_TransferFile;
msg.type_ = nim::kNIMMessageTypeCustom;
msg.receiver_accid_ = remote_flag;
msg.sender_accid_ = LoginManager::GetInstance()->GetAccount();
// 确保发送出去的消息 id 和构建 bubble 使用的消息 id 都是相同的
// 在加载历史消息时才能根据消息 id 匹配到对应的数据
msg.client_msg_id_ = session_id;
msg.msg_setting_.resend_flag_ = nim::BS_FALSE;
msg.timetag_ = 1000 * nbase::Time::Now().ToTimeT();
msg.status_ = nim::kNIMMsgLogStatusSending;
msg.msg_setting_.server_history_saved_ = nim::BS_FALSE;//不存云端
msg.msg_setting_.roaming_ = nim::BS_FALSE;//不漫游
msg.msg_setting_.self_sync_ = nim::BS_FALSE;//不进行多端同步
msg.msg_setting_.need_push_ = nim::BS_FALSE;//不推送
msg.msg_setting_.push_need_badge_ = nim::BS_FALSE;//不计数
msg.msg_setting_.need_offline_ = nim::BS_FALSE;//不需要支持离线
msg.msg_setting_.routable_ = nim::BS_FALSE;//不需要抄送
msg.attach_ = values.toStyledString();
nim::Talk::SendMsg(msg.ToJsonString(true));
}
return true;
}
收到文件传输请求
当另一端收到了 IM 消息后,首先会根据当前消息的类型判断是否是传输文件的请求,如果是,则义无反顾的将消息传递给之前我们初始化时 SDK 返回给我们的命令通道。C++ 封装层提供了一个 OnReceiveChannelCommand 的方法来将接收到的消息传递给 SDK。
// 如果收到的消息是 P2P 传送文件消息,那么抛到注入的命令通道来执行
Json::Value values;
Json::Reader reader;
bool parse_success = reader.parse(msg.attach_, values);
bool is_transfer_file = values.isMember("type") && values["type"].asInt() == nim_comp::CustomMsgType_TransferFile;
if (msg.type_ == nim::kNIMMessageTypeCustom && parse_success && is_transfer_file)
{
QLOG_APP(L"Receive transfer file notification, command type = {0}") << values[kJsonKeyCommand].asString();
// 协商成功后才能收到这个自定义的传送文件请求
nim_p2p::NimP2PDvelopKit::GetInstance()->OnReceiveChannelCommand((RemoteFlagType)msg.sender_accid_.c_str(), msg.attach_);
}
SDK 在解析这个请求后,会回调到上层我们在初始化时注册好的 接收到文件传输请求的回调函数
,对应示例代码中的 nim_comp::P2PCallback::OnTransferFileRequest
,在该回调函数中,我们根据 SDK 回调回来的要传输的文件名、文件大小、session id 等信息,在应用界面上创建对应的 UI 信息。
void P2PCallback::OnTransferFileRequest(const RemoteFlagType remote_flag, TransferFileSessionID session_id, const char* file_info)
{
SessionBox* session_box = dynamic_cast<SessionBox*>(SessionManager::GetInstance()->FindSessionBox(remote_flag));
if (session_box)
{
nim::IMMessage msg;
Json::Value root;
Json::Value json_file_info;
Json::Reader reader;
root["type"] = CustomMsgType_TransferFile;
if (reader.parse(file_info, json_file_info))
{
root["params"]["file_info"] = json_file_info;
root["session_id"] = session_id;
}
// 注意这里构建消息自己是接收端,对端是 remote_flag
msg.type_ = nim::kNIMMessageTypeCustom;
msg.receiver_accid_ = LoginManager::GetInstance()->GetAccount();
msg.sender_accid_ = remote_flag;
msg.client_msg_id_ = session_id;
msg.msg_setting_.resend_flag_ = nim::BS_FALSE;
msg.timetag_ = 1000 * nbase::Time::Now().ToTimeT();
msg.status_ = nim::kNIMMsgLogStatusSent;
msg.msg_setting_.server_history_saved_ = nim::BS_FALSE;//不存云端
msg.msg_setting_.roaming_ = nim::BS_FALSE;//不漫游
msg.msg_setting_.self_sync_ = nim::BS_FALSE;//不进行多端同步
msg.msg_setting_.need_push_ = nim::BS_FALSE;//不推送
msg.msg_setting_.push_need_badge_ = nim::BS_FALSE;//不计数
msg.msg_setting_.need_offline_ = nim::BS_FALSE;//不需要支持离线
msg.msg_setting_.routable_ = nim::BS_FALSE;//不需要抄送
msg.attach_ = root.toStyledString();
session_box->ShowMsg(msg, false, false);
}
}
接收或拒绝
当接收端在界面做了对应展示后,用户就可以选择接收或拒绝接收文件的操作了,调用 SDK 提供的相应方法即可。接收和拒绝所需的 session id 就是接收端收到的 session id。
// 接收文件
nim_p2p::NimP2PDvelopKit::GetInstance()->ReceiveFile(const_cast<TransferFileSessionID>(transfer_file_session_id_.c_str()), local_path_);
// 拒绝文件
nim_p2p::NimP2PDvelopKit::GetInstance()->RejectReceiveFile(const_cast<TransferFileSessionID>(transfer_file_session_id_.c_str()));
无论接收还是拒绝,SDK 都会回调上层注册的发送命令通道 nim_comp::P2PCallback::SendCommandChannel
,通道中只需将消息包装为所需的消息发送格式传递给另一方即可。
处理文件传输状态和进度
当接收方开始接收文件后,SDK 会在底层开始协商传输方案相互建立连接开始传送文件,每当有文件状态和传输进度变更时,都会回调到上层在初始化时注册好的回调函数,分别对应 nim_comp::P2PCallback::OnTransferFileSessionStateChangeCallback
和 nim_comp::P2PCallback::OnTransferFileProgressCallback
用户拒绝、取消接收文件,文件传输完成、失败等状态均会回调到状态变化的回调函数中。应用可根据不同的状态和对应的 session id 修改传送文件的界面变化。进度展示也是同理。
接收中取消发送或取消接收
接收文件过程中发送方和接收方都可以主动取消文件传输,发送方使用如下方法取消传送文件:
nim_p2p::NimP2PDvelopKit::GetInstance()->CancelTransferFile(const_cast<TransferFileSessionID>(transfer_file_session_id_.c_str()));
接收方主动取消文件传输:
nim_p2p::NimP2PDvelopKit::GetInstance()->CancelReceiveFile(const_cast<TransferFileSessionID>(transfer_file_session_id_.c_str()));
清理工作
当应用程序退出或者需要反初始化 NIM P2P 模块时,调用范初始化相关函数即可
nim_p2p::NimP2PDvelopKit::GetInstance()->UnInit();